﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Drawing;
using System.Xml.Serialization;
using System.Diagnostics;
using IndicoInterface.SimpleAgendaDataModel;
using DTBuilderLib.VisualHierarchyClasses;
using Utilities;
using Microsoft;
using System.Xml;

namespace DTBuilderLib
{
    /// <summary>
    /// Manage the interface to the deep zoom composer utilities.
    /// </summary>
    public class DeepZoomBuilder
    {

        /// <summary>
        /// The base directory where talks should be rendered
        /// </summary>
        public string BuildDirectory { get; set; }

        /// <summary>
        /// The image quality... 0 to 1.
        /// </summary>
        public double ImageQuality { get; set; }

        /// <summary>
        /// Set defaults.
        /// </summary>
        public DeepZoomBuilder()
        {
            BuildDirectory = Path.GetTempPath() + "\\DeepZoomTalkRenders";
            ImageQuality = 1.0;
        }

        /// <summary>
        /// Given the root visual item, render it to XML and then run the composer utilties
        /// on it. Leave behind a token file to 
        /// </summary>
        /// <param name="root">The visual hierarchy to render to a compser scene.</param>
        /// <returns>Directory that contains the item.bin file - output from the composer.</returns>
        public DirectoryInfo RenderVisual(string meetingname, Visual root)
        {
            ///
            /// We are outta here if there is a null sitting in here!
            /// 

            if (root == null)
            {
                return null;
            }

            ///
            /// Make sure to add some info onto the exception adn log it on the way out -- but we don't block the
            /// exception with the following try block.
            /// 

            try
            {
                DirectoryInfo meetingDir = GetMeetingBaseDirectory(meetingname);
                if (TokenFileExists(meetingDir))
                {
                    return meetingDir;
                }
                meetingDir.Create();

                ///
                /// 
                /// Rescal so that the max size is 1.0. The reaons has to do with the sparse image tool's
                /// coordinate system. A size of 1.0 means that the image takes up the full view port.
                /// What is what we want, of course.
                /// 
                /// The system seems to use the width as the measuring stick, and so we only scale
                /// w.r.t. the width rather than making sure the whole thing fits on the display
                /// screen.
                /// 

                double scale = 1.0 / root.Width;
                root.Scale(scale);

                ///
                /// Next task is we have to move through the hierarchy looking for visuals that are compositions. And any
                /// images that aren't gathered into compositions we need to put into 
                /// 

                var allTopLevelImages = from v in root.GetHierarchicalEnumerator(v => v.IsCompositionUnit)
                                        select v;

                ///
                /// Generate the meta-data info and also the composite image. We do this in a single step so
                /// we can screen out the meta data for the images that fail to convert (for whatever reason).
                /// 

                var topLevelImagesInfo = from v in allTopLevelImages
                                         select new 
                                         {
                                             metaData = new DZTalkVisualMetaData
                                             {
                                             Title = v.Title,
                                             Width = v.Width,
                                             Height = v.Height,
                                             X = v.XPosition,
                                             Y = v.YPosition,
                                             Z = v.ZIndex - 1
                                             },
                                             ImageFInfo = ConvertComposition(v, meetingDir)
                                         };


                var goodTopLevelImages = (from v in topLevelImagesInfo where v.ImageFInfo != null select v).ToArray();
                var imageMetadataList = (from v in goodTopLevelImages select v.metaData).ToArray();
                using (var text = File.CreateText(meetingDir.FullName + "\\meetingMetadata.xml"))
                {
                    XmlSerializer xs = new XmlSerializer(typeof(DZTalkVisualMetaData[]));
                    xs.Serialize(text, imageMetadataList);
                    text.Close();
                }

                ///
                /// Now, put everything else together in a final image
                /// 

                Microsoft.DeepZoomTools.CollectionCreator cr = new Microsoft.DeepZoomTools.CollectionCreator();
                cr.TileSize = 256;
                cr.TileFormat = Microsoft.DeepZoomTools.ImageFormat.Jpg;
                cr.MaxLevel = 8;
                cr.ImageQuality = ImageQuality;

                var imagesAsStrings = from i in goodTopLevelImages
                                      select i.ImageFInfo.FullName;
                cr.Create(imagesAsStrings.ToArray(), meetingDir.FullName + @"\" + meetingDir.Name);

                ///
                /// Write a token file so we don't have to do this again...
                /// 

                SetToken(meetingDir);

                return meetingDir;
            }
            catch (Exception e)
            {
                Logger.LogErrorMessage("Error Rendering", "Error rendering meeting " + meetingname + " - " + root.Title, e);
                throw;
            }
        }

        /// <summary>
        /// This visual is a sparse image -- run it, and return the result!
        /// </summary>
        /// <param name="v"></param>
        /// <returns></returns>
        private FileInfo ConvertComposition(Visual root, DirectoryInfo meetingDir)
        {
            ///
            /// Rescale it to be the proper size...
            /// 

            double oldWidth = root.Width;
            double scale = 1.0 / oldWidth;
            root.Scale(scale);

            double oldX = root.XPosition;
            double oldY = root.YPosition;
            root.XPosition = 0.0;
            root.YPosition = 0.0;

            string baseName = Path.GetRandomFileName();
            if (root.ShortNameGood)
            {
                baseName = root.ShortName;
            }

            ///
            /// Create a single sparse image too for this guy
            /// 

            try
            {
                Microsoft.DeepZoomTools.SparseImageCreator si = new Microsoft.DeepZoomTools.SparseImageCreator();
                si.CopyMetadata = false;
                si.ImageQuality = ImageQuality;
                si.TileFormat = Microsoft.DeepZoomTools.ImageFormat.Jpg;
                si.TileSize = 256;
                si.TileOverlap = 1;
                var imageList = ConvertVisualsToImages(root).ToArray();

                baseName = Path.GetFileNameWithoutExtension(baseName);
                string dirName = meetingDir.FullName + @"\sc\" + baseName;

                ///
                /// Get the scalling back
                /// 

                root.Scale(oldWidth);
                root.XPosition = oldX;
                root.YPosition = oldY;

                ///
                /// No need to re-run if the file already exists and the token file is also there!
                /// 

                FileInfo finalXMLFile = new FileInfo(Path.ChangeExtension(dirName, ".xml"));
                FileInfo finalTokenFile = new FileInfo(Path.ChangeExtension(dirName, ".txt"));
                if (finalXMLFile.Exists && finalTokenFile.Exists)
                {
                    return finalXMLFile;
                }

                si.Create(imageList, dirName);

                finalXMLFile.Refresh();

                using (var writer = finalTokenFile.CreateText())
                {
                    writer.WriteLine("Rendered");
                    writer.Close();
                }

                return finalXMLFile;
            }
            catch (Exception e)
            {
                Logger.LogErrorMessage("SubMeetingRender", "Error rendering submeeting in directory " + baseName, e);
                return null;
            }
        }

        /// <summary>
        /// Returns an iterator that will convert all the visuals to Image objects suitable for
        /// stuffing into a image ceator (or whatnot).
        /// </summary>
        /// <returns></returns>
        IEnumerable<Microsoft.DeepZoomTools.Image> ConvertVisualsToImages(Visual root)
        {
            if (root == null)
            {
                root = new VisualHolder();
            }
            var result = from s in root.SGNodeList
                         orderby s.ZOrder ascending
                         select ConvertSGToImage(s);
            return result;
        }

        /// <summary>
        /// Converts a single sceen graph node to a list.
        /// 
        /// The coordinate system used by images is a bit... errr, funny.
        /// First (0,0) is at the upper left hand corner. Right and down are both negative. So it is like looking
        /// at the lower right hand corner of a traditional x,y coordinate system. Now the x-position is
        /// really how many "widths" away it is from the origin. For example, if it is half the size of
        /// the view port, then a value of -1 means it is starts half way across the screen!! So each
        /// image, in this sense, has a seperate coordinate system. And the y coordinate uses the x as
        /// its "basis" units. Fu-barrred!!!
        /// 
        /// Second you have to tell it how big to make it. A value of 1.0 means that it will fill the entire
        /// view port (0->1). A value of 2 means it will be half, etc. Basically, 1/size. This is stored in
        /// ViewPortWidth.
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private Microsoft.DeepZoomTools.Image ConvertSGToImage(SceneGraphSceneNode s)
        {
            ///
            /// The coordinate system that is understood by the sparse image tool is not your Dad's coordinate system.
            /// 
            /// The second problem is the size.

            Microsoft.DeepZoomTools.Image result = new Microsoft.DeepZoomTools.Image(s.FileName);

            ///
            /// For the view port width we assume that the max width is 1.0 for everything we are doing here.
            /// And that in the mean time this was done with a global rescaling. So the width of this image
            /// is going to be just 1/ its view port width.
            /// 

            result.ViewportWidth = 1.0/s.Width;

            ///
            /// Now we have to figure out how many "widths" over and down this guy is positioned
            /// and that will be the position.
            /// 

            result.ViewportOrigin = new System.Windows.Point(
                -(s.x / s.Width), -(s.y / s.Width)
                );

            return result;
        }

        /// <summary>
        /// Generate the token file info - so we can use it all over.
        /// </summary>
        /// <param name="talkName"></param>
        /// <returns></returns>
        private FileInfo GetTokenFileInfo(string talkName)
        {
            return new FileInfo(GetMeetingBaseDirectory(talkName).FullName + "\\token.txt");
        }

        /// <summary>
        /// Generate the token file info - so we can use it all over.
        /// </summary>
        /// <param name="talkName"></param>
        /// <returns></returns>
        private FileInfo GetTokenFileInfo(DirectoryInfo talkDir)
        {
            return new FileInfo(talkDir.FullName + "\\token.txt");
        }

        /// <summary>
        /// See if the token file exists. If so, then the process was run already
        /// and we don't need to re-run it.
        /// </summary>
        /// <param name="meetingDir"></param>
        /// <returns></returns>
        private bool TokenFileExists(DirectoryInfo talkDir)
        {
            FileInfo tokenInfo = GetTokenFileInfo(talkDir);
            return tokenInfo.Exists;
        }

        /// <summary>
        /// Set the token!
        /// </summary>
        /// <param name="talkDir"></param>
        private void SetToken(DirectoryInfo talkDir)
        {
            FileInfo tokenInfo = GetTokenFileInfo(talkDir);
            using (TextWriter wr = tokenInfo.CreateText())
            {
                wr.WriteLine("Process ran");
            }
        }

        /// <summary>
        /// Allows someone externally to set the token for reprocessing.
        /// </summary>
        /// <param name="meetingName"></param>
        public void SetToken(string meetingName)
        {
            SetToken(GetMeetingBaseDirectory(meetingName));
        }


        /// <summary>
        /// External method that allows for the manipulation of the token.
        /// </summary>
        /// <param name="meetingName"></param>
        public void ClearToken(string meetingName)
        {
            ClearToken(GetMeetingBaseDirectory(meetingName));
        }

        /// <summary>
        /// Clear the token file out.
        /// </summary>
        /// <param name="meetingDir"></param>
        public void ClearToken(DirectoryInfo meetingDir)
        {
            FileInfo f = GetTokenFileInfo(meetingDir);
            if (f.Exists)
            {
                f.Delete();
            }
        }

        /// <summary>
        /// Have we already rendred this talk?
        /// </summary>
        /// <param name="talkname"></param>
        /// <returns></returns>
        public bool MeetingAlreadyRendered(string talkname)
        {
            return GetTokenFileInfo(talkname).Exists;
        }

        /// <summary>
        /// Returns the base directory where we load up talk stuff given the meeting name.
        /// </summary>
        /// <param name="meetingname"></param>
        /// <returns></returns>
        private DirectoryInfo GetMeetingBaseDirectory(string meetingname)
        {
            return new DirectoryInfo(BuildDirectory + "\\" + meetingname);
        }

        /// <summary>
        /// What was the output directory for this rendered talk?
        /// </summary>
        /// <param name="talkname"></param>
        /// <returns></returns>
        public DirectoryInfo RenderedMeetingDirectory(string talkname)
        {
            var dr = GetMeetingBaseDirectory(talkname);
            if (!dr.Exists)
            {
                throw new DTException("Meeting '" + talkname + "' has not been rendered - can't return directory for it!");
            }
            return dr;
        }
    }
}
